package ms.core.otp;

import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import ms.core.tool.Log4j2Tool;
import ms.core.tool.StrTool;

public class TotpTool {
	private TotpTool() {
	}

	/**
	 * 该方法使用JCE提供加密算法。 HMAC使用加密哈希算法作为参数计算哈希消息认证码。
	 * 
	 * @param crypto:   the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
	 * @param keyBytes: 用于HMAC密钥的字节
	 * @param text:     用于HMAC密钥的字节数
	 */
	private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
		try {
			Mac hmac;
			hmac = Mac.getInstance(crypto);
			SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
			hmac.init(macKey);
			return hmac.doFinal(text);
		} catch (GeneralSecurityException gse) {
			throw new UndeclaredThrowableException(gse);
		}
	}

	/**
	 * This method converts a HEX string to Byte[]
	 * 
	 * @param hex: the HEX string
	 * @return: a byte array
	 */
	private static byte[] hexStr2Bytes(String hex) {
		// Adding one byte to get the right conversion Values starting with "0" can be
		// converted
		byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();

		// Copy all the REAL bytes, not the "first"
		byte[] ret = new byte[bArray.length - 1];
		for (int i = 0; i < ret.length; i++)
			ret[i] = bArray[i + 1];
		return ret;
	}

	private static final int[] DIGITS_POWER
	// 0 1 2 3 4 5 6 7 8
			= { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };

	/**
	 * This method generates a TOTP value for the given set of parameters.
	 *
	 * @param key:          the shared secret, HEX encoded
	 * @param time:         a value that reflects a time
	 * @param returnDigits: number of digits to return
	 *
	 * @return: a numeric String in base 10 that includes truncationDigits digits
	 */
	public static String generateTOTP(String key, String time, String returnDigits) {
		return generateTOTP(key, time, returnDigits, "HmacSHA1");
	}

	/**
	 * This method generates a TOTP value for the given set of parameters.
	 *
	 * @param key:          the shared secret, HEX encoded
	 * @param time:         a value that reflects a time
	 * @param returnDigits: number of digits to return
	 *
	 * @return: a numeric String in base 10 that includes truncationDigits digits
	 */
	public static String generateTOTP256(String key, String time, String returnDigits) {
		return generateTOTP(key, time, returnDigits, "HmacSHA256");
	}

	/**
	 * This method generates a TOTP value for the given set of parameters.
	 *
	 * @param key:          the shared secret, HEX encoded
	 * @param time:         a value that reflects a time
	 * @param returnDigits: number of digits to return
	 *
	 * @return: a numeric String in base 10 that includes truncationDigits digits
	 */
	public static String generateTOTP512(String key, String time, String returnDigits) {
		return generateTOTP(key, time, returnDigits, "HmacSHA512");
	}

	/**
	 * This method generates a TOTP value for the given set of parameters.
	 *
	 * @param key:          the shared secret, HEX encoded
	 * @param time:         a value that reflects a time
	 * @param returnDigits: number of digits to return
	 * @param crypto:       the crypto function to use
	 *
	 * @return: a numeric String in base 10 that includes truncationDigits digits
	 */
	public static String generateTOTP(String key, String time, String returnDigits, String crypto) {
		int codeDigits = Integer.decode(returnDigits).intValue();
		String result = null;

		// Using the counter
		// First 8 bytes are for the movingFactor
		// Compliant with base RFC 4226 (HOTP)
		while (time.length() < 16)
			time = "0" + time;

		// Get the HEX in a Byte[]
		byte[] msg = hexStr2Bytes(time);
		byte[] k = hexStr2Bytes(key);

		byte[] hash = hmac_sha(crypto, k, msg);

		// put selected bytes into result int
		int offset = hash[hash.length - 1] & 0xf;

		int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16)
				| ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);

		int otp = binary % DIGITS_POWER[codeDigits];

		result = Integer.toString(otp);
		while (result.length() < codeDigits) {
			result = "0" + result;
		}
		return result;
	}

	/**
	 * 验证动态口令是否正确
	 * 
	 * @param secretBase32 密钥
	 * @param code         待验证的动态口令
	 * @return
	 */
	public static boolean verify(String secretBase32, String code) {
		return generate(secretBase32).equals(code);
	}

	/**
	 * 生成totp协议字符串
	 * 
	 * @param accoName 帐号，如 abc@126.com
	 * @param domain 平台名称
	 * @param secretBase32 密钥
	 * @return
	 */
	public static String generateTotpString(String accoName, String domain, String secretBase32) {
		return StrTool.isBlank(domain) ? "otpauth://totp/" + accoName + "?secret=" + secretBase32 :
										 "otpauth://totp/" + domain + ":" + accoName + "?secret=" + secretBase32+"&issuer="+domain;
	}

	/**
	 * 根据密钥生成动态口令
	 * 
	 * @param secretBase32 base32编码格式的密钥
	 * @return
	 */
	public static String generate(String secretBase32) {

		String secretHex = "";
		try {
			secretHex = HexEncoding.encode(Base32String.decode(secretBase32));
		} catch (Base32String.DecodingException e) {
			Log4j2Tool.error(TotpTool.class, "解码" + secretBase32 + "出错，", e);
			throw new RuntimeException("解码Base32出错");
		}

		long X = 30;

		String steps = "0";
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		df.setTimeZone(TimeZone.getTimeZone("UTC"));

		long currentTime = System.currentTimeMillis() / 1000L;
		try {
			long t = currentTime / X;
			steps = Long.toHexString(t).toUpperCase();
			while (steps.length() < 16)
				steps = "0" + steps;

			return generateTOTP(secretHex, steps, "6", "HmacSHA1");
		} catch (final Exception e) {
			Log4j2Tool.error(TotpTool.class, "生成动态口令出错：" + secretBase32, e);
			throw new RuntimeException("生成动态口令出错");
		}
	}

	/**
	 * 生成base32编码的随机密钥
	 * 
	 * @param length
	 * @return
	 */
	public static String getRandomSecretBase32(int length) {
		SecureRandom random = new SecureRandom();
		byte[] salt = new byte[length / 2];
		random.nextBytes(salt);
		return Base32String.encode(salt);
	}
}
